// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using SnapObjects.Commons;

namespace SnapObjects.Data.PostgreSql
{
    internal class PostgreSqlUpdateSqlGenerator : UpdateSqlGenerator
    {
        public PostgreSqlUpdateSqlGenerator(PostgreSqlSqlGenerationHelper postgreSqlSqlGenerationHelper) :
            base(postgreSqlSqlGenerationHelper)
        {
        }

        public StringBuilder AppendSqlHeader(StringBuilder sql)
        {
            return sql.Append("WITH rows AS(");
        }

        public StringBuilder AppendSqlTail(StringBuilder sql)
        {
            return sql.Append(" RETURNING 1) SELECT CAST(count(*) AS INTEGER) FROM rows ;");
        }

        public override ResultSetMapping AppendRawUpdateSql(
            [NotNull] StringBuilder sqlStringBuilder, [NotNull] string rawUpdateSql, int sqlPosition)
        {
            var isCurdSql = rawUpdateSql.StartsWith("insert", StringComparison.OrdinalIgnoreCase)
                || rawUpdateSql.StartsWith("delete", StringComparison.OrdinalIgnoreCase)
                || rawUpdateSql.StartsWith("update", StringComparison.OrdinalIgnoreCase);

            if (isCurdSql)
            {
                var arr = rawUpdateSql.Split(';');

                for (var i = 0; i < arr.Length; i++)
                {
                    this.AppendSqlHeader(sqlStringBuilder);

                    sqlStringBuilder.Append(arr[i]);

                    this.AppendSqlTail(sqlStringBuilder);
                }
            }
            else
            {
                sqlStringBuilder.Append(rawUpdateSql);
            }

            return ResultSetMapping.RowCountAndLastInResultSet;
        }

        protected override ResultSetMapping AppendSelectAffectedCountSql(
            StringBuilder sqlStringBuilder, string name, string schema, int sqlPosition)
        {
            return ResultSetMapping.RowCountAndLastInResultSet;
        }

        protected override ResultSetMapping AppendSelectAffectedSql(
            [NotNull] StringBuilder sqlStringBuilder,
            [NotNull] string name,
            [CanBeNull] string schema,
            [NotNull] IReadOnlyList<ColumnModification> readOperations,
            [NotNull] IReadOnlyList<ColumnModification> conditionOperations,
            int sqlPosition)
        {
            Check.NotNull(sqlStringBuilder, nameof(sqlStringBuilder));
            Check.NotEmpty(name, nameof(name));
            Check.NotNull(readOperations, nameof(readOperations));
            Check.NotNull(conditionOperations, nameof(conditionOperations));

            var fileds = new StringBuilder();

            sqlStringBuilder.Append(" returning ");

            foreach (var readOperation in readOperations)
            {
                if (fileds.Length > 0)
                {
                    fileds.Append(",");
                }

                var field = this.SqlGenerationHelper.DelimitIdentifier(readOperation.MappedColumn);

                fileds.Append(field);
            }

            sqlStringBuilder.Append(fileds)
                .Append(this.SqlGenerationHelper.StatementTerminator).AppendLine();

            sqlStringBuilder.Append("\n");

            return ResultSetMapping.SelectAndLastInResultSet;
        }

        public override void AppendBatchHeader(StringBuilder sqlStringBuilder)
        {
            sqlStringBuilder.Append("SET NOCOUNT ON")
                .Append(this.SqlGenerationHelper.StatementTerminator).AppendLine();
        }

        protected override void AppendIdentityWhereCondition(
            StringBuilder sqlStringBuilder, ColumnModification columnModification)
        {
        }

        protected override void AppendRowsAffectedWhereCondition(StringBuilder sqlStringBuilder, int expectedRowsAffected)
        {
            var sql = sqlStringBuilder.ToString();

            sqlStringBuilder = sqlStringBuilder.Remove(0, sqlStringBuilder.Length);

            this.AppendSqlHeader(sqlStringBuilder);

            sqlStringBuilder.Append(sql);

            this.AppendSqlTail(sqlStringBuilder);
        }

        public override ResultSetMapping AppendInsertSql(
            [NotNull] StringBuilder sqlStringBuilder, [NotNull] ModelUpdateInfo modelUpdateInfo, int sqlPosition)
        {
            Check.NotNull(sqlStringBuilder, nameof(sqlStringBuilder));
            Check.NotNull(modelUpdateInfo, nameof(modelUpdateInfo));

            var name = modelUpdateInfo.Table.ToString();
            var schema = modelUpdateInfo.Schema;
            var operations = modelUpdateInfo.ColumnModifications;
            var writeOperations = operations.Where(o => o.IsWrite).ToList();
            var readOperations = operations.Where(o => o.IsRead).ToList();

            this.AppendInsertSql(sqlStringBuilder, name, schema, writeOperations);

            if (readOperations.Count > 0)
            {
                var keyOperations = operations
                    .Where(o => o.IsKey)
                    .ToList();

                return this.AppendSelectAffectedSql(
                    sqlStringBuilder, name, schema, readOperations, keyOperations, sqlPosition);
            }
            else
            {
                return ResultSetMapping.RowCountAndLastInResultSet;
            }
        }

        public override ResultSetMapping AppendDeleteSql(
            [NotNull] StringBuilder sqlStringBuilder, [NotNull] ModelUpdateInfo modelUpdateInfo, int sqlPosition)
        {
            Check.NotNull(sqlStringBuilder, nameof(sqlStringBuilder));
            Check.NotNull(modelUpdateInfo, nameof(modelUpdateInfo));

            var name = modelUpdateInfo.Table.ToString();
            var schema = modelUpdateInfo.Schema;
            var conditionOperations = modelUpdateInfo.ColumnModifications.Where(o => o.IsCondition).ToList();

            if (!String.IsNullOrWhiteSpace(sqlStringBuilder.ToString()))
            {
                var preSql = sqlStringBuilder.ToString();

                sqlStringBuilder.Remove(0, sqlStringBuilder.Length);

                var sb = new StringBuilder();

                this.AppendDeleteHeader(sb, name, schema);

                this.AppendWhereClause(sb, conditionOperations);

                sqlStringBuilder.Append(preSql);

                this.AppendSqlHeader(sqlStringBuilder);

                sqlStringBuilder.Append(sb);

                this.AppendSqlTail(sqlStringBuilder);
            }
            else
            {
                this.AppendDeleteHeader(sqlStringBuilder, name, schema);

                this.AppendWhereClause(sqlStringBuilder, conditionOperations);

                var sql = sqlStringBuilder.ToString();

                sqlStringBuilder.Remove(0, sqlStringBuilder.Length);

                this.AppendSqlHeader(sqlStringBuilder);

                sqlStringBuilder.Append(sql);

                this.AppendSqlTail(sqlStringBuilder);
            }

            return ResultSetMapping.RowCountAndLastInResultSet;
        }

        protected override void AppendInsertSql(
            [NotNull] StringBuilder sqlStringBuilder,
            [NotNull] string name,
            [CanBeNull] string schema,
            [NotNull] IReadOnlyList<ColumnModification> writeOperations)
        {
            Check.NotNull(sqlStringBuilder, nameof(sqlStringBuilder));
            Check.NotEmpty(name, nameof(name));
            Check.NotNull(writeOperations, nameof(writeOperations));

            this.AppendInsertHeader(sqlStringBuilder, name, schema, writeOperations);

            this.AppendValuesHeader(sqlStringBuilder, writeOperations);

            this.AppendValues(sqlStringBuilder, writeOperations);
        }

        protected override void AppendInsertHeader(
            [NotNull] StringBuilder sqlStringBuilder,
            [NotNull] string name,
            [CanBeNull] string schema,
            [NotNull] IReadOnlyList<ColumnModification> operations)
        {
            Check.NotNull(sqlStringBuilder, nameof(sqlStringBuilder));
            Check.NotEmpty(name, nameof(name));
            Check.NotNull(operations, nameof(operations));

            sqlStringBuilder.Append("INSERT INTO ");

            this.SqlGenerationHelper.DelimitIdentifier(sqlStringBuilder, name, schema);

            if (operations.Count > 0)
            {
                sqlStringBuilder
                    .Append(" (")
                    .AppendValues(operations,
                        this.SqlGenerationHelper,
                        (sb, o, helper) => helper.DelimitIdentifier(sb, o.MappedColumn))
                    .Append(")");
            }
        }

        public override ResultSetMapping AppendUpdateSql(
            [NotNull] StringBuilder sqlStringBuilder,
            [NotNull] ModelUpdateInfo modelUpdateInfo,
            int sqlPosition)
        {
            Check.NotNull(sqlStringBuilder, nameof(sqlStringBuilder));
            Check.NotNull(modelUpdateInfo, nameof(modelUpdateInfo));

            var name = modelUpdateInfo.Table;
            var schema = modelUpdateInfo.Schema;
            var operations = modelUpdateInfo.ColumnModifications;

            var writeOperations = operations.Where(o => o.IsWrite).ToList();

            var conditionOperations = operations.Where(o => o.IsCondition).ToList();

            var readOperations = operations.Where(o => o.IsRead).ToList();

            if (writeOperations.Count > 0)
            {
                this.AppendUpdateSql(sqlStringBuilder, name, schema, writeOperations, conditionOperations);
            }

            return ResultSetMapping.RowCountAndLastInResultSet;
        }

        protected override void AppendUpdateSql(
            [NotNull] StringBuilder sqlStringBuilder,
            [NotNull] string name,
            [CanBeNull] string schema,
            [NotNull] IReadOnlyList<ColumnModification> writeOperations,
            [NotNull] IReadOnlyList<ColumnModification> conditionOperations)
        {
            Check.NotNull(sqlStringBuilder, nameof(sqlStringBuilder));
            Check.NotEmpty(name, nameof(name));
            Check.NotEmpty(writeOperations, nameof(writeOperations));
            Check.NotNull(conditionOperations, nameof(conditionOperations));

            this.AppendUpdateHeader(sqlStringBuilder, name, schema, writeOperations);

            this.AppendWhereClause(sqlStringBuilder, conditionOperations);

            sqlStringBuilder.Append(" ");

            this.AppendSqlTail(sqlStringBuilder);

            sqlStringBuilder.Append(this.SqlGenerationHelper.StatementTerminator).AppendLine();
        }

        protected override void AppendUpdateHeader(
            [NotNull] StringBuilder sqlStringBuilder,
            [NotNull] string name,
            [CanBeNull] string schema,
            [NotNull] IReadOnlyList<ColumnModification> operations)
        {
            Check.NotNull(sqlStringBuilder, nameof(sqlStringBuilder));
            Check.NotEmpty(name, nameof(name));
            Check.NotNull(operations, nameof(operations));

            this.AppendSqlHeader(sqlStringBuilder).Append("UPDATE ");

            this.SqlGenerationHelper.DelimitIdentifier(sqlStringBuilder, name, schema);

            sqlStringBuilder.Append(" SET ")
                .AppendValues(
                    operations,
                    this.SqlGenerationHelper,
                    (sb, o, helper) =>
                    {
                        helper.DelimitIdentifier(sb, o.MappedColumn);

                        sb.Append(" = ");

                        helper.GenerateParameterName(sb, o.ParameterName);
                    });
        }

        protected override void AppendWhereCondition(
            [NotNull] StringBuilder sqlStringBuilder,
            [NotNull] ColumnModification columnModification,
            bool useOriginalValue)
        {
            Check.NotNull(sqlStringBuilder, nameof(sqlStringBuilder));
            Check.NotNull(columnModification, nameof(columnModification));

            this.SqlGenerationHelper.DelimitIdentifier(sqlStringBuilder, columnModification.MappedColumn);

            var parameterValue = useOriginalValue ?
                columnModification.OriginalValue : columnModification.CurrentValue;

            if (parameterValue == null)
            {
                sqlStringBuilder.Append(" IS NULL");
            }
            else
            {
                sqlStringBuilder.Append(" = ");

                this.SqlGenerationHelper.GenerateParameterName(sqlStringBuilder, useOriginalValue ?
                    columnModification.OriginalParameterName : columnModification.ParameterName);
            }
        }
    }
}
